﻿/*
 * Copyright 2010 Marc Cesarine
 * 
 * This file is part of Ssh Commander.

 * Ssh Commander is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.

 * Ssh Commander is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.

 * You should have received a copy of the GNU General Public License
 * along with Ssh Commander.  If not, see <http://www.gnu.org/licenses/>.
 */

using System;
using System.Collections;
using System.Collections.Generic;
using System.Collections.ObjectModel;
using System.ComponentModel;
using System.Net;
using System.Net.Sockets;
using System.Threading;
using Tamir.SharpSsh.jsch;

namespace SshCommander
{
    public class Server : INotifyPropertyChanged
    {
        private string id_v;
        private string name_v;
        private string hostname_v;
        private string tunnel_hostname_v;
        private string tunnel_username_v;
        private string tunnel_password_v;
        private int tunnel_remoteport_v;
        private int tunnel_localport_v;
        private Session session;
        private statut statut_v;
        private int port_v;
        private ObservableCollection<Account> accounts_v;
        private string superusername_v;
        private string superuserpass_v;
        private string command_v;
        private string result_v;
        private Object Lock_v;
        private List<int> usedports_v;
        private bool isTunneled_v;
        private ManualResetEvent doneevent_v;

        #region constructor

        public Server()
        {
            initObject();
        }

        public Server(ManualResetEvent doneevent)
        {
            doneevent_v = doneevent;
            initObject();
        }

        #endregion

        private void initObject()
        {
            statut_v = statut.ARecetter;
            port_v = 22;
            OnPropertyChanged("statut");
            OnPropertyChanged("port");
            accounts_v = new ObservableCollection<Account>();
            Lock_v = new Object();
            isTunneled_v = false;
            tunnel_remoteport_v = 22;
        }

        #region getters and setters

        public string id
        {
            get
            {
                return id_v;
            }
            set
            {
                id_v = value;
                OnPropertyChanged("id");
            }
        }

        public string name
        {
            get
            {
                return name_v;
            }
            set
            {
                name_v = value;
                OnPropertyChanged("name");
            }
        }

        public string hostname
        {
            get
            {
                return hostname_v;
            }
            set
            {
                hostname_v = value;
                OnPropertyChanged("hostname");
            }
        }

        public string superusername
        {
            get
            {
                return superusername_v;
            }
            set
            {
                superusername_v = value;
                OnPropertyChanged("superusername");
            }
        }

        public string superuserpass
        {
            get
            {
                return superuserpass_v;
            }
            set
            {
                superuserpass_v = value;
                OnPropertyChanged("superuserpass");
            }
        }

        public statut teststatut
        {
            get
            {
                return statut_v;
            }
            set
            {
                statut_v = value;
                OnPropertyChanged("teststatut");
            }
        }

        public int port
        {
            get
            {
                return port_v;
            }
            set
            {
                port_v = value;
                OnPropertyChanged("port");
            }
        }

        public string tunnel_hostname
        {
            get
            {
                return tunnel_hostname_v;
            }
            set
            {
                tunnel_hostname_v = value;
                OnPropertyChanged("tunnel_hostname_v");
            }
        }

        public string tunnel_username
        {
            get
            {
                return tunnel_username_v;
            }
            set
            {
                tunnel_username_v = value;
                OnPropertyChanged("tunnel_username_v");
            }
        }

        public string tunnel_password
        {
            get
            {
                return tunnel_password_v;
            }
            set
            {
                tunnel_password_v = value;
                OnPropertyChanged("tunnel_password_v");
            }
        }

        public int tunnel_remoteport
        {
            get
            {
                return tunnel_remoteport_v;
            }
            set
            {
                tunnel_remoteport_v = value;
                OnPropertyChanged("tunnel_remoteport_v");
            }
        }

        public int tunnel_localport
        {
            get
            {
                return tunnel_localport_v;
            }
        }

        public ManualResetEvent doneevent
        {
            get
            {
                return doneevent_v;
            }
            set
            {
                doneevent_v = value;
            }
        }

        #endregion

        public void setUsedPorts(ref List<int> usedports)
        {
            usedports_v = usedports;
        }

        public ObservableCollection<Account> accounts
        {
            get
            {
                return accounts_v;
            }
        }

        public string command
        {
            get
            {
                return command_v;
            }
            set
            {
                command_v = value;
                OnPropertyChanged("command");
            }
        }

        public string result
        {
            get
            {
                return result_v;
            }
            set
            {
                result_v = value;
                OnPropertyChanged("result");
            }
        }

        public enum statut
        {
            [Description("OK")]
            OK = 1,
            [Description("KO")]
            KO = 2,
            [Description("A recetter")]
            ARecetter = 3,
            [Description("En cours")]
            EnCours = 4
        }

        public bool isTunneled
        {
            get
            {
                return isTunneled_v;
            }
            set
            {
                isTunneled_v = value;

                // If the server connection is tunneled, we set all hostnames to check as "localhost"
                // to test throught tunnel
                if (isTunneled_v)
                {
                    foreach (Account account in accounts_v)
                    {
                        account.hostname = "localhost";
                    }
                }
            }
        }

        // Wrapper method for use with thread pool
        public void ConnectivityThreadPoolCallback(Object threadContext)
        {
            TcpClient client = null;

            try
            {
                teststatut = statut.EnCours;
                client = new TcpClient(hostname_v, port_v);
                teststatut = statut.OK;
            }
            catch (Exception)
            {
                teststatut = statut.KO;
            }
            finally
            {
                if (client != null)
                {
                    client.Close();
                }
                doneevent_v.Set();
            }
        }

        // Wrapper method for use with thread pool
        public void ConnectivityTunnelThreadPoolCallback(Object threadContext)
        {
            teststatut = statut.EnCours;

            //Preparation of session to tunnel host server
            JSch jsch = new JSch();
            session = jsch.getSession(tunnel_username_v, tunnel_hostname_v, tunnel_remoteport_v);
            session.setHost(tunnel_hostname_v);
            session.setPassword(tunnel_password_v);
            Hashtable config = new Hashtable(1);
            config.Add("StrictHostKeyChecking", "no");
            session.setConfig(config);

            try
            {
                //Connection to tunnel host server
                session.connect();

                if (session.isConnected())
                {
                    int portnumber = GetFreePort();

                    //Set the tunnel local port
                    tunnel_localport_v = portnumber;
                    OnPropertyChanged("tunnel_localport_v");

                    TcpClient client = null;
                    try
                    {
                        //Set port forwarding on the opened session
                        session.setPortForwardingL(tunnel_localport_v, "localhost", tunnel_remoteport_v);

                        //We check connectivity via tunnel
                        client = new TcpClient("localhost", tunnel_localport_v);
                        teststatut = statut.OK;

                        //If port was successfully working from beginning to complete end, we free it
                        //else, it will be kept tagged reserved
                        usedports_v.Remove(portnumber);
                    }
                    catch (Exception)
                    {
                        //We set the status to KO
                        teststatut = statut.KO;
                    }
                    finally
                    {
                        client.Close();
                    }
                }
                else
                {
                    //We set the status to KO
                    teststatut = statut.KO;
                }
            }
            catch (Exception)
            {
                //If any error, we set the status to KO
                teststatut = statut.KO;
            }
            finally
            {
                if ((session != null) && (session.isConnected()))
                {
                    session.disconnect();
                }
                doneevent_v.Set();
            }
        }

        // Wrapper method for use with thread pool
        public void PasswordsCheckThreadPoolCallback(Object threadContext)
        {
            if ((hostname_v != "") && (accounts_v.Count > 0))
            {
                foreach (Account account in accounts_v)
                {
                    ThreadPool.QueueUserWorkItem(account.PasswordCheckThreadPoolCallBack);
                }
                doneevent_v.Set();
            }
        }

        // Wrapper method for use with thread pool
        public void PasswordsCheckTunnelThreadPoolCallback(Object threadContext)
        {
            if ((hostname_v != "") && (accounts_v.Count > 0))
            {
                //Preparation of session to tunnel host server
                JSch jsch = new JSch();
                session = jsch.getSession(tunnel_username_v, tunnel_hostname_v, tunnel_remoteport_v);
                session.setHost(tunnel_hostname_v);
                session.setPassword(tunnel_password_v);
                Hashtable config = new Hashtable(1);
                config.Add("StrictHostKeyChecking", "no");
                session.setConfig(config);

                try
                {
                    //Connection to tunnel host server
                    session.connect();

                    if (session.isConnected())
                    {
                        int portnumber = GetFreePort();

                        //Set the tunnel local port
                        foreach (Account account in accounts_v)
                        {
                            account.port = portnumber;
                            ThreadPool.QueueUserWorkItem(account.PasswordCheckThreadPoolCallBack);
                        }
                    }
                }
                catch (Exception)
                {

                }
                finally
                {
                    if ((session != null) && (session.isConnected()))
                    {
                        session.disconnect();
                    }
                    doneevent_v.Set();
                }

            }
        }

        private int GetFreePort()
        {
            bool portfound = false;
            int portnumber = 50001;

            //Loop for finding free local port
            while (!portfound)
            {
                bool portreserved = true;

                //Lock the common already reserved ports list
                lock (Lock_v)
                {
                    //Find any non reserved port in common list
                    while (portreserved)
                    {
                        if (!usedports_v.Contains(portnumber))
                        {
                            //Reserve first free port in common list
                            portreserved = false;
                            usedports_v.Add(portnumber);
                        }
                        else
                        {
                            portnumber++;
                        }
                    }
                }

                //Check if non reserved port is available
                if (PortIsAvailable(portnumber))
                {
                    //If yes, we exit the loop,we got the port
                    portfound = true;
                }
                //If no, we let reserved actual port (because it's not working) and try another
            }

            return portnumber;
        }

        private bool PortIsAvailable(int port)
        {
            bool result = false;

            TcpClient tcpclient = null;
            TcpListener tcplistener = null;
            try
            {
                IPAddress localhostaddress = IPAddress.Parse("127.0.0.1");
                tcplistener = new TcpListener(localhostaddress, port);
                tcplistener.Start();
                tcpclient = new TcpClient();
                tcpclient.Connect("localhost", port);

                if (tcpclient.Connected)
                {
                    result = true;
                }
            }
            catch (Exception)
            {
                result = false;
            }
            finally
            {
                tcpclient.Close();
                tcplistener.Stop();
            }

            return result;
        }

        #region INotifyPropertyChanged Membres

        public event PropertyChangedEventHandler PropertyChanged;
        protected void OnPropertyChanged(string propertyname)
        {
            if (this.PropertyChanged != null)
                PropertyChanged(this, new PropertyChangedEventArgs(propertyname));
        }

        #endregion
    }
}
